4.3 编译器的优化与调试
编译器优化在之前的章节我们有提到过,当时我们讲解了局部优化。
本节我们将进行更多的讲解,了解编译器的优化也可以帮助我们提高开发效率,在开发中即解决一些使用问题。
本节代码存放目录为 lesson11
编译器优化技术
编译器优化是指编译器在生成最终的机器代码之前,对代码进行的一系列改进,以提高程序的执行效率或减少内存占用。下面我们将展示几种优化方式以帮助我们思考。
内联展开
内联展开是指将函数调用替换为函数体本身,从而避免函数调用的开销。Go
编译器会自动决定是否进行内联展开,通常是针对那些短小、频繁调用的函数。
func add(a, b int) int {
return a + b
}
func main() {
result := add(2, 3) // 编译器可能会将此调用内联展开
fmt.Println(result)
}
优化后的可能情况:
resultO := 2 + 3
fmt.Println(resultO)
还有可能进一步优化为:
fmt.Println(5)
在上面的代码中,直接将函数调用替换为了2+3
,这样肯定是更简单直接的。
循环优化
循环优化包括多种技术,如循环展开、循环合并、循环分割和循环交换等。
循环展开:通过展开循环体,减少循环的迭代次数,从而降低循环控制的开销。
循环合并:将多个独立的循环合并为一个,以减少循环控制的开销。
循环分割:将一个复杂的循环拆分为多个简单的循环,提高并行化的可能性。
循环交换:调整循环嵌套的顺序,以提高缓存命中率或并行化。
示例如下所示:
for i := 0; i < 100; i++ {
// 原始代码
}
编译器可能会将其优化为:
for i := 0; i < 100; i += 2 {
// 展开后的循环
// 第1次迭代
// 第2次迭代
}
死代码消除
编译器会删除那些永远不会执行的代码或对程序结果无影响的代码,从而减少不必要的指令和数据。
示例如下所示:
func main() {
var x = 10
if false {
fmt.Println("This code will never run")
}
}
编译器会识别并删除if false
条件下的代码块,因为它永远不会执行。
常量折叠和传播
编译器会在编译时计算表达式的常量值,并将它们直接替换到代码中,从而减少运行时的计算。
示例如下所示:
const a = 3
const b = 4
const c = a * b // 编译器会将c的值直接替换为12
编译参数与调优
Go
编译器提供了一些编译参数,可以帮助开发者控制编译行为,从而进行性能调优或调试。
-gcflags
:可以用来传递编译器的优化标志。例如,go build -gcflags="-N -l"
会禁用优化和内联展开,用于调试。-ldflags
:用于传递给链接器的标志,可以控制最终可执行文件的大小和行为。-race
:用于检测数据竞争问题,在并发代码中非常有用。
示例如下所示:
go build -gcflags="-N -l" -o example lesson12.go
此命令禁用了优化和内联展开,用于生成更易于调试的代码。
我们来测试一下,首先我们允许优化进行编译,我们查看汇编代码:
go tool objdump -s main example
在输出中我们会找到下面的指令:
MOVD $5, R0
上面的指令就直接将5
加载到寄存器,之后进行输出操作。
那么我么再禁用优化后观察一下:
go build -gcflags="-N -l" -o example lesson12.go
我们再次查看汇编代码,会发现之前很多没有的东西都展示出来了,这就说明了这是没有经过优化的。
我们可以看到这样的输出:
MOVD $5, R3
MOVD R3, 40(RSP)
这其实执行的就是:
resultO := 2 + 3
调试编译器行为
调试编译器行为有助于理解编译器如何优化代码,并帮助我们识别潜在的性能问题或不必要的开销。
我们在之前的章节中提到过pprof
,这是比较方便的工具,使用也比较简单,可以对照官方示例尝试使用。
另外我们可以通过查看编译器的输出了解更多内容。
go tool compile -S
:生成汇编代码,帮助我们了解编译器的优化决策。例如:
go tool compile -S lesson12.go
go tool objdump
:反汇编工具,用于分析可执行文件的汇编代码。这我们在上文也讲到过,主要就是用于查看到编译后的汇编代码。
我们还可以使用go build -gcflags="-m"
可以让编译器输出优化决策信息,例如是否进行了内联展开,是否消除了某些代码等。
如下所示:
go build -gcflags="-m" lesson12.go
结果输出如下:
# command-line-arguments
./lesson12.go:11:6: can inline add
./lesson12.go:16:15: inlining call to add
./lesson12.go:17:13: inlining call to fmt.Println
./lesson12.go:20:13: inlining call to fmt.Println
./lesson12.go:22:13: inlining call to fmt.Println
./lesson12.go:17:13: ... argument does not escape
./lesson12.go:17:13: result escapes to heap
./lesson12.go:20:13: ... argument does not escape
./lesson12.go:20:13: resultO escapes to heap
./lesson12.go:22:13: ... argument does not escape
./lesson12.go:22:14: c escapes to heap
执行命令后编译器为我们输出了优化决策信息,例如./lesson12.go:11:6: can inline add
告诉我们add
函数可以被内联处理;c escapes to heap
告诉我们c
被分配到了堆内存上。
小结
理解编译的优化其实主要是帮助我们了解官方的优化逻辑,这样我们在实际开发的时候就可以按照这种风格去进行,那么我们的程序性能也就可以得到一定的保证。
另外还有一些关于输出包体大小的内容,本书不打算进行讲解。我们认为Go
语言的性能以及它所提供的便利,足以弥补了编译包体大小的问题。